Skip to content

Commit bc4378e

Browse files
committed
properly handle revision based_on field
1 parent b2b7ce4 commit bc4378e

File tree

4 files changed

+117
-14
lines changed

4 files changed

+117
-14
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Generated by Django 4.2.20 on 2025-03-27 15:45
2+
3+
from django.db import migrations
4+
5+
6+
def ensure_empty_based_on_for_non_translations(apps, schema_editor):
7+
"""
8+
Although historically the "based_on" of revisions of documents without a
9+
parent (mostly English documents but there also exist a few non-English
10+
documents without a parent) has often been set to an earlier revision
11+
within the same document, it has never been used for these revisions.
12+
From this point forward, we want to ensure that the "based_on" of a
13+
revision can be non-NULL only if the revision is a translation of an
14+
English revision, or in other words, only for revisions of documents
15+
with a parent. So we'll set the "based_on" to NULL for any revision
16+
that is not a translation of an English revision, or in other words,
17+
for any revision of a document without a parent.
18+
"""
19+
Revision = apps.get_model("wiki", "Revision")
20+
Revision.objects.filter(document__parent__isnull=True, based_on__isnull=False).update(
21+
based_on=None
22+
)
23+
24+
25+
class Migration(migrations.Migration):
26+
27+
dependencies = [
28+
("wiki", "0018_alter_document_restrict_to_groups"),
29+
]
30+
31+
operations = [
32+
migrations.RunPython(
33+
ensure_empty_based_on_for_non_translations, migrations.RunPython.noop
34+
),
35+
]

kitsune/wiki/models.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -861,20 +861,21 @@ class Meta(object):
861861
]
862862

863863
def _based_on_is_clean(self):
864-
"""Return a tuple: (the correct value of based_on, whether the old
865-
value was correct).
866-
867-
based_on must be a revision of the English version of the document. If
868-
based_on is not already set when this is called, the return value
869-
defaults to something reasonable.
864+
"""
865+
Returns a tuple:
866+
(the correct value of "based_on", whether the old value was correct)
870867
868+
The value of "based_on" should only be non-NULL for revisions of documents
869+
with a parent, or in other words, revisions that are translations of an
870+
English revision, and in those cases its value should always be a revision
871+
of its document's parent (an English document).
871872
"""
872-
original = self.document.original
873-
if self.based_on and self.based_on.document != original:
874-
# based_on is set and points to the wrong doc. The following is
875-
# then the most likely helpful value:
876-
return original.localizable_or_latest_revision(), False
877-
# Even None is permissible, for example in the case of a brand new doc.
873+
if self.document.parent:
874+
if not self.based_on or (self.based_on.document != self.document.parent):
875+
return self.document.parent.localizable_or_latest_revision(), False
876+
elif self.based_on:
877+
return None, False
878+
878879
return self.based_on, True
879880

880881
def clean(self):
@@ -965,7 +966,9 @@ def latest_revision(excluded_rev, constraint):
965966
except IndexError:
966967
return None
967968

968-
Revision.objects.filter(based_on=self).update(based_on=None)
969+
# This is a safeguard, since these revisions should already have a "based_on" of None.
970+
Revision.objects.filter(document__parent__isnull=True, based_on=self).update(based_on=None)
971+
969972
document = self.document
970973

971974
# If the current_revision is being deleted, try to update it to the

kitsune/wiki/tests/test_handlers.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,68 @@ def test_mixed_revisions_user_deletion(self):
205205

206206
rev2 = Revision.objects.filter(id=rev2_id).first()
207207
self.assertIsNone(rev2, "Deleted user's revision should be deleted")
208+
209+
def test_revision_based_on_deletion(self):
210+
"""
211+
Test the handling of a revision's "based_on" field when it references a revision
212+
created by a user that is deleted.
213+
"""
214+
doc = DocumentFactory()
215+
rev1 = RevisionFactory(document=doc, creator=self.user)
216+
rev2 = RevisionFactory(document=doc, based_on=rev1)
217+
rev3 = ApprovedRevisionFactory(document=doc, based_on=rev2)
218+
219+
doc_id = doc.id
220+
rev1_id = rev1.id
221+
rev2_id = rev2.id
222+
rev3_id = rev3.id
223+
224+
self.listener.on_user_deletion(self.user)
225+
226+
self.assertTrue(
227+
Document.objects.filter(id=doc_id).exists(), "Document should not be deleted"
228+
)
229+
self.assertFalse(
230+
Revision.objects.filter(id=rev1_id).exists(),
231+
"Deleted user's unapproved revision should be deleted",
232+
)
233+
rev2 = Revision.objects.filter(id=rev2_id).first()
234+
self.assertIsNotNone(rev2, "Other user's revision should not be deleted")
235+
self.assertIsNone(rev2.based_on, "Other user's revision's based_on should now be None")
236+
rev3 = Revision.objects.filter(id=rev3_id).first()
237+
self.assertIsNotNone(rev3, "The approved revision should not be deleted")
238+
self.assertEqual(rev3.based_on, rev2)
239+
240+
it_doc = DocumentFactory(locale="it", parent=doc)
241+
it_rev = RevisionFactory(document=it_doc, based_on=rev2)
242+
243+
it_doc_id = it_doc.id
244+
it_rev_id = it_rev.id
245+
246+
self.listener.on_user_deletion(rev2.creator)
247+
248+
# Ensure that the default document wasn't deleted.
249+
self.assertTrue(
250+
Document.objects.filter(id=doc_id).exists(), "Document should not be deleted"
251+
)
252+
253+
# The deleted user's un-approved revision should not have been deleted,
254+
# but instead re-assigned to the sumo bot, because it's the based-on
255+
# value of a localized revision.
256+
rev2 = Revision.objects.filter(id=rev2_id).first()
257+
self.assertIsNotNone(rev2, "Deleted user's revision should not be deleted")
258+
self.assertEqual(rev2.creator.username, settings.SUMO_BOT_USERNAME)
259+
260+
# Ensure that the third revision, whose based-on value is the revision
261+
# whose creator was deleted, was not affected at all.
262+
rev3 = Revision.objects.filter(id=rev3_id).first()
263+
self.assertIsNotNone(rev3, "The approved revision should not be deleted")
264+
self.assertEqual(rev3.based_on, rev2)
265+
266+
self.assertTrue(
267+
Document.objects.filter(id=it_doc_id).exists(),
268+
"The Italian document should not be deleted",
269+
)
270+
it_rev = Revision.objects.filter(id=it_rev_id).first()
271+
self.assertIsNotNone(it_rev, "The Italian revision should not be deleted")
272+
self.assertEqual(it_rev.based_on, rev2)

kitsune/wiki/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ def edit_document(request, document_slug, revision_id=None):
551551
return init_check
552552
user, doc, rev = init_check
553553

554-
rev_form = RevisionForm(instance=rev, initial={"based_on": rev.id, "comment": ""})
554+
rev_form = RevisionForm(instance=rev, initial={"based_on": None, "comment": ""})
555555

556556
# POST
557557
if request.method == "POST":

0 commit comments

Comments
 (0)